package com.pas.book;

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Vector;
import javax.microedition.lcdui.List;

/**
 * Class for representation of one book in the library.
 *
 * @author Tom? Darmovzal [tomas.darmovzal (at) seznam.cz]
 * @author Josef Cacek [josef.cacek (at) atlas.cz]
 * @author $Author: kwart $
 * @version $Revision: 1.14 $
 * @created $Date: 2007/08/30 11:25:22 $
 */
public class BookData {

    /**
     * book identifier - generated from book name and used for directory name
     */
    private String id;

    /**
     * book name
     */
    public String name;

    /**
     * book description
     */
    public String description;
    protected int size;
    private int partSize;
    private boolean rightToLeft;

    private BufferHolder buffer;
    private static BufferHolder nextBuff;
    private static BufferThread bfThread = null;

    private int index;

    /**
     * Class which reads part of book to buffer.
     *
     * @author Josef Cacek
     */
    static class BufferHolder {
        private char[] buffer;
        volatile int bufferLength;
        volatile int partNo;
        private String path;

        /**
         * Constructor in which one part of book is readed to buffer
         *
         * @param aPart number of part which should be read
         */
        public BufferHolder(final int aPart, final int aSize, final String aID) {
            partNo = aPart;
            bufferLength = aSize;
            path = "/" + Utils.DATA_FOLDER + "/" + aID + "/" + Utils.PART_FILE_PREFIX + partNo;
        }

        /**
         * Reads buffer
         *
         * @throws IOException
         */
        public void read() throws IOException {

            Utils.debug("BufferHolder.read() started");
            InputStream tInStream = getClass().getResourceAsStream(path);
            //bj check if a data block is there
            if (tInStream == null) {
                return;
            }
            Reader tmpReader = new Win1251Reader(tInStream);
            char[] tmpBuff = new char[bufferLength];
            bufferLength = tmpReader.read(tmpBuff);
            setBuffer(tmpBuff);
            tmpReader.close();
            Utils.debug("BufferHolder.read() finished");
        }

        public synchronized char[] getBuffer() {
            return buffer;
        }

        private synchronized void setBuffer(char[] aBuff) {
            buffer = aBuff;
        }

        public String toString() {
            if (Utils.DEBUG) {
                return "buffer: " + new String(buffer) + "\nbufferLength: " + bufferLength +
                        "\npartNo: " + partNo + "\npath: " + path;
            }
            return super.toString();
        }
    }

    /**
     * Class for reading next part of book in separate thread (prebuffering).
     *
     * @author Josef Cacek
     */
    static class BufferThread extends Thread {

        private BufferHolder holder;

        /**
         * Constructor
         *
         * @param aPart number of next part
         */
        public BufferThread(final int aPart, final int aSize, final String aId) {
            setHolder(new BufferHolder(aPart, aSize, aId));
            setBfThread(this);
        }

        /* (non-Javadoc)
           * @see java.lang.Runnable#run()
           */
        public void run() {
            Utils.debug("BufferThread.run() started");
            try {
                getHolder().read();
            } catch (IOException e) {
                if (Utils.ERROR) {
                    Utils.error("BufferThread.run() IOException: " + e, e);
                }
            }
            setNextBuff(getHolder());
            setBfThread(null);
            Utils.debug("BufferThread.run() finished");
        }

        /**
         * Sets buffer
         *
         * @param aHolder
         */
        public synchronized void setHolder(final BufferHolder aHolder) {
            holder = aHolder;
        }

        /**
         * Returns buffer
         */
        public synchronized BufferHolder getHolder() {
            return holder;
        }
    }

    /**
     * Reads book with given name
     *
     * @param aName name of the book
     */
    public BookData(String aName) {
        name = aName;
        InputStream tmpIS = open(Utils.INFO_FILE);
        if (tmpIS == null) {
            if (Utils.WARN) {
                Utils.warn("Book constructor -  Cannot get book info");
            }
            throw new RuntimeException("Cannot get book info");
        }
        DataInputStream tmpDIS = new DataInputStream(tmpIS);
        try {
            size = tmpDIS.readInt();
            partSize = tmpDIS.readInt();
            description = tmpDIS.readUTF();
            rightToLeft = tmpDIS.readBoolean();
            tmpDIS.close();
        } catch (IOException ioe) {
            if (Utils.ERROR) {
                Utils.error("read Book info failed: " + ioe.getMessage(), ioe);
            }
            throw new RuntimeException(ioe.getMessage());
        }
    }

    /**
     * Opens inputstream from file in current book directory.
     *
     * @param aPath filename to open
     * @return InputStream for given path
     */
    protected InputStream open(String aPath) {
        return (getClass()).getResourceAsStream("/" + Utils.DATA_FOLDER + "/" + getId() + "/" + aPath);
    }

    /**
     * Fills given part into buffer and runs reading of next part on background
     *
     * @param aPart number of part which should be saved to buffer
     * @throws Exception
     */
    protected synchronized void fillBuffer(final int aPart) throws Exception {
        Utils.debug("Book.fillBuffer() started");
        if (aPart * partSize > size - 1) {
            Utils.debug("Book.fillBuffer() - End of book.");
            throw new EOFException();
        }
        while (getBfThread() != null) {
            Utils.debug("Book.fillBuffer() - Waiting for buffer thread.");
            Thread.sleep(200);
        }
        BufferHolder tmpNext = getNextBuff();
        if (tmpNext != null &&
                aPart == tmpNext.partNo) {
            buffer = tmpNext;
            setNextBuff(null);
        } else {
            buffer = new BufferHolder(aPart, partSize, getId());
            buffer.read();
        }
        Utils.debug("Book.fillBuffer() starting BufferThread");
        BufferThread tmpThread = new BufferThread(aPart + 1, partSize, getId());
        tmpThread.setPriority(2);
        tmpThread.start();
        Utils.debug("Book.fillBuffer() finished");
    }

    /**
     * Returns character and moves bufferOffset backward
     *
     * @return current character
     * @throws Exception
     */
    public char readPrev() throws Exception {
        final char tmpChr = readCurr();
        index--;
        return tmpChr;
    }

    /**
     * Returns character and moves bufferOffset ahead
     *
     * @return current character
     * @throws Exception
     */
    public char readNext() throws Exception {
        char tmpChr = readCurr();
        index++;
        return tmpChr;
    }

    /**
     * Returns character from buffer (on bufferOffset position). If it's
     * necessary reads new data to buffer (prev/next).
     *
     * @throws Exception
     */
    protected char readCurr() throws Exception {
        if (getPosition() == size || getPosition() == -1) {
            Utils.debug("readCurr EOFException");
            throw new EOFException();
        }
        if (index == -1) {
            rollPrev();
            index = buffer.bufferLength - 1;
        } else if (index == buffer.bufferLength) {
            rollNext();
            index = 0;
        }
        Utils.debug("Book.readCurr() - " + buffer.getBuffer()[index]);
        return buffer.getBuffer()[index];
    }

    /**
     * Reads previous part of book to buffer
     *
     * @throws Exception
     */
    protected void rollPrev() throws Exception {
        if (buffer == null || buffer.partNo <= 0) {
            if (Utils.ERROR) {
                Utils.error("rollPrev problem\n" + buffer);
            }
            throw new Exception("Cannot roll previous buffer");
        }
        rollBuffer(buffer.partNo - 1);
    }

    /**
     * Reads next part of book to buffer
     *
     * @throws Exception
     */
    protected void rollNext() throws Exception {
        if (buffer == null || (buffer.partNo + 1) * partSize >= size) {
            if (Utils.ERROR) {
                Utils.error("rollNext problem\n" + buffer);
            }

            throw new Exception("Cannot roll next buffer");
        }
        rollBuffer(buffer.partNo + 1);
    }

    /**
     * Reads part of text (starting on position aPart * partSize) to
     * buffer
     *
     * @param aPart which part of book should be read
     * @throws Exception
     */
    protected void rollBuffer(final int aPart) throws Exception {
        if (buffer != null && aPart == buffer.partNo) {
            return;
        }
        if (aPart < 0) {
            throw new RuntimeException("Negative buffer num");
        }
        int tmpBuffMax = size / partSize;
        if (size % partSize == 0) {
            tmpBuffMax--;
        }
        if (aPart > tmpBuffMax) {
            throw new RuntimeException("Buffer num too large");
        }
        fillBuffer(aPart);
    }

    /**
     * Returns position of actual character in book.
     *
     * @return position of actual character in book
     */
    public synchronized int getPosition() {
        return buffer == null ? 0 : buffer.partNo * partSize + index;
    }

    /**
     * Sets new actual position in book (moves buffer and bufferOffset)
     *
     * @param aPos
     */
    public synchronized int setPosition(int aPos) {
        Utils.debug("Book - setPosition: " + aPos);
        if (aPos < 0) {
            aPos = 0;
        } else if (aPos >= size) {
            aPos = size - 1;
        }
        try {
            rollBuffer(aPos / partSize);
            index = aPos % partSize;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
        return aPos;
    }

    /**
     * Sets position in book from the given percentage
     *
     * @param aPerc
     * @throws Exception
     */
    public void setPercPosition(int aPerc) throws Exception {
        if (aPerc < 0) {
            aPerc = 0;
        } else if (aPerc > 100) {
            aPerc = 100;
        }
        setPosition((size - 1) * aPerc / 100);
    }

    /*
      * (non-Javadoc)
      *
      * @see java.lang.Object#toString()
      */
    public String toString() {
        return "Book (id=" + id + ", name=\"" + name + "\", size=" + size
                + ", position=" + getPosition() + ")";
    }

    /**
     * Returns ID of book.
     *
     * @return ID of book
     * @see Utils#createIdFromName(String)
     */
    public String getId() {
        if (id == null) {
            id = Utils.createIdFromName(name);
        }
        return id;
    }

    public synchronized static BufferHolder getNextBuff() {
        return nextBuff;
    }

    public synchronized static void setNextBuff(BufferHolder nextBuff) {
        BookData.nextBuff = nextBuff;
    }

    public synchronized static BufferThread getBfThread() {
        return bfThread;
    }

    public synchronized static void setBfThread(BufferThread bfThread) {
        BookData.bfThread = bfThread;
    }

    public boolean isChapterFile() {
        InputStream tmpIS = open(Utils.CHAPTERS_FILE);
        if (tmpIS == null) {
            return false;
        }
        try {
            tmpIS.close();
        } catch (IOException e) {
            Utils.error(e);
        }
        return true;
    }

    public void getChapters(List aList, Vector anIdxVector) {
        InputStream tmpIS = open(Utils.CHAPTERS_FILE);
        if (tmpIS == null) {
            if (Utils.WARN) {
                Utils.warn("No chapter file's present.");
            }
        } else {
            DataInputStream tmpDIS = new DataInputStream(tmpIS);
            try {
                final int tmpCount = tmpDIS.readInt();
                if (Utils.INFO) {
                    Utils.info(tmpCount + " chapters available");
                }
                for (int i = 0; i < tmpCount; i++) {
                    aList.append(tmpDIS.readUTF(), null);
                    anIdxVector.addElement(new Integer(tmpDIS.readInt()));
                }
                tmpDIS.close();
            } catch (IOException ioe) {
                if (Utils.ERROR) {
                    Utils.error("getChapters() failed : " + ioe.getMessage(), ioe);
                }
                throw new RuntimeException(ioe.getMessage());
            }
        }
    }

    public boolean isRightToLeft() {
        return rightToLeft;
    }

    public void setRightToLeft(boolean rightToLeft) {
        this.rightToLeft = rightToLeft;
    }
}
